Skip to content
Echarts 动态图表导出 GIF
概述
ECharts 的入场动画和数据更新过渡效果依赖浏览器的渲染周期,不会持久化到图片中。如果要将动态图表分享为 GIF 或视频,一种直接的做法是在动画期间持续截取画布快照,再将多帧图像合成为动画。最常用的实现是采用 setInterval 定时调用 getDataURL() 获取 Base64 编码的 PNG 帧,然后交由 gifshot.js 生成 GIF。这条路径在时间精度、编码效率和动画完整性上存在难以弥补的缺陷。以下说明该方案的实际表现并列出可替代的实现路径。
基本概念
ECharts 的快照接口
ECharts 实例提供了 getDataURL 方法,将当前 Canvas 内容导出为 Data URL。该方法可以指定图片类型、像素比和背景色:
js
const dataUrl = chartInstance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff',
});返回值为 Base64 编码的 PNG。getDataURL 是同步操作,直接读取 Canvas 像素数据。
帧捕获与 GIF 合成
定时调用 getDataURL 得到图像序列,再交给 GIF 编码库合成动画。以 gifshot 为例,合成过程包括颜色量化、抖动处理和 LZW 压缩,所有计算都在浏览器主线程执行。
定时器精度
setInterval 和 setTimeout 的名义间隔并不可靠:
- 非活动标签页中,定时器会被节流到最低 1000ms。
- 活动标签页中,嵌套超过 5 层的定时器有 4ms 最小延迟。
- 宏任务队列中的其他任务会推迟回调的实际执行时间。
因此,帧间隔不可控,帧率会随标签页状态剧烈变化。
定时截取方案
流程为:启动动画 → 开启 setInterval 循环 → 每次回调执行 getDataURL 并将结果存入数组 → 动画结束时停止定时器 → 将帧序列传给 gifshot.createGIF → 下载 GIF。
基本示例:
js
const frames = [];
const interval = 50; // 目标 20fps
let timer;
function startCapture() {
timer = setInterval(() => {
frames.push(chartInstance.getDataURL({ type: 'png' }));
}, interval);
}
// 触发动画并开始捕获
chartInstance.dispatchAction({ type: 'downplay' });
startCapture();
// 监听动画结束
chartInstance.on('finished', () => {
clearInterval(timer);
gifshot.createGIF(
{
images: frames,
gifWidth: 800,
gifHeight: 600,
},
(obj) => {
if (!obj.error) {
const link = document.createElement('a');
link.download = 'chart.gif';
link.href = obj.image;
link.click();
}
}
);
});chartInstance.on('finished') 用于检测动画结束,避免截取过多或遗漏帧。不同动画类型触发 finished 的时机不同:setOption 的过渡动画、dispatchAction 触发的强调/下钻动画结束后都会触发。如果动画是连续更新的动态数据,则需由业务侧条件判断来停止捕获。
限制
时间轴抖动
setInterval(fn, 50) 很难保持稳定的 20fps。标签页切到后台后,定时器可能降低到 1fps,导致动画中间帧大量丢失。回到前台后,收集到的帧序列也无法还原原始动画的节奏——部分中间状态被跳过,生成的 GIF 会出现跳帧。即使在活动标签页中,宏任务调度造成的偏移也会让实际间隔在 48ms ~ 150ms 之间波动,具体取决于页面中其他任务的执行耗时。
编码开销与内存
Base64 编码的 PNG 比原始 PNG 体积大约 33%。假设单帧 800×600 的 PNG 约 150KB,Base64 后约 200KB,60 帧序列的内存占用约为 12MB。gifshot 在主线程对 60 帧 800×600 的图像序列进行颜色量化与 LZW 压缩,通常需要 5~15 秒,期间页面无法响应用户操作。若在编码过程中进行 DOM 操作,阻塞时间还会延长。
动画完整性
ECharts 的动画帧不会与定时器同步。理想情况下,捕获应发生在每次渲染帧绘制完成之后,但 setInterval 无法绑定到 requestAnimationFrame 的节奏上。实际捕获的帧序列可能夹杂重复帧或缺失关键过渡帧,视觉效果表现为“跳跃”而非平滑过渡。
替代方案
使用 Puppeteer 精确控制帧捕获
在 Node.js 中通过 Puppeteer 启动无头 Chromium,加载包含 ECharts 图表的页面,触发动效后在每一帧调用 page.screenshot 截图。帧间隔由 setTimeout 控制,不会受浏览器标签页节流影响(Puppeteer 使用独立的渲染进程)。截取到的 PNG 可直接写入磁盘,后续由 ffmpeg 合成为 GIF 或视频。
逐帧截图示例:
js
const puppeteer = require('puppeteer');
const fs = require('fs');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/chart');
await page.waitForSelector('#chart-container');
// 触发动画
await page.click('#animate-btn');
// 等待动画启动
await page.waitForTimeout(100);
const frameCount = 90; // 3秒 × 30fps
for (let i = 0; i < frameCount; i++) {
const buffer = await page.screenshot({
type: 'png',
clip: { x: 0, y: 0, width: 800, height: 600 }
});
fs.writeFileSync(`frame_${String(i).padStart(3, '0')}.png`, buffer);
await new Promise(r => setTimeout(r, 33)); // ~30fps
}
await browser.close();
})();截图以 frame_000.png、frame_001.png 等命名后,可使用 ffmpeg 生成 GIF(通过调色板优化以提升画质):
bash
ffmpeg -framerate 30 -i frame_%03d.png \
-vf "fps=30,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
-loop 0 output.gif如果输出 H.264 视频:
bash
ffmpeg -framerate 30 -i frame_%03d.png -c:v libx264 -pix_fmt yuv420p output.mp4Puppeteer 方案适合需要精确帧率和高质量输出的自动化场景,但依赖 Node.js 服务端环境,Chromium 体积约 300MB。
使用 MediaRecorder 从 Canvas 录制
直接从 ECharts 使用的 <canvas> 元素获取媒体流,调用浏览器的硬件编码器生成视频文件,帧率可稳定在 60fps,编码在独立线程(硬件加速)上执行,不会阻塞主线程。
js
const canvas = chartInstance.getDom().querySelector('canvas');
const stream = canvas.captureStream(60);
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9',
});
const chunks = [];
recorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'chart.webm';
a.click();
};
// 动画开始前启动录制
recorder.start();
chartInstance.dispatchAction({ type: 'highlight', seriesIndex: 0 });
// 动画结束后停止
chartInstance.on('finished', () => {
recorder.stop();
});captureStream() 仅适用于 Canvas 渲染器。如果页面使用了 SVG 渲染模式,不会存在对应的 <canvas> 元素,也就无法通过该方式获取媒体流。
浏览器的兼容情况:Chromium 系支持 canvas.captureStream(60) 并支持 VP8/VP9 编码;Firefox 同样支持。Safari(14.1+)支持 MediaRecorder,但对 video/webm 的编码支持有限,通常需回退到 video/mp4:
js
let mimeType = 'video/webm;codecs=vp9';
if (!MediaRecorder.isTypeSupported(mimeType)) {
mimeType = 'video/mp4';
}Safari 输出的 MP4 有时缺少 MOOV atom 导致无法直接播放,需通过 ffmpeg 执行 -movflags faststart 修复。
系统录屏工具
对于一次性分享的场景,直接使用系统录屏工具(OBS Studio、macOS 屏幕录制等)录制图表区域是最简单的替代方式。无需编写代码即可获得高质量视频,缺点是难以集成到自动化导出流程。
注意点
定时截取配合 gifshot 的方案仅适合理解帧捕获机制的教学演示,不宜用于需要可靠输出的项目。Puppeteer 方案适合服务端自动化与高质量输出,MediaRecorder 方案适合客户端实时录制,系统录屏适合临时性需求。
参考链接
- ECharts
getDataURL方法:https://echarts.apache.org/zh/api.html#echartsInstance.getDataURL - gifshot 库:https://github.com/yahoo/gifshot
- Puppeteer 文档:https://pptr.dev/
- ffmpeg 滤镜(palettegen/paletteuse):https://ffmpeg.org/ffmpeg-filters.html#palettegen
- MediaRecorder API:https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder
- canvas.captureStream:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/captureStream
